Skip to content

feat(AVM): Fail on incomplete toRadix decompositions#17233

Merged
sirasistant merged 14 commits intomerge-train/avmfrom
arv/to_radix_decomposition
Sep 24, 2025
Merged

feat(AVM): Fail on incomplete toRadix decompositions#17233
sirasistant merged 14 commits intomerge-train/avmfrom
arv/to_radix_decomposition

Conversation

@sirasistant
Copy link
Contributor

@sirasistant sirasistant commented Sep 23, 2025

Resolves #17144

@sirasistant sirasistant changed the base branch from next to merge-train/avm September 23, 2025 14:29
auto value = fr::random_element();
uint32_t num_limbs = static_cast<uint32_t>(state.range(0));
uint32_t radix = static_cast<uint32_t>(state.range(1));
uint256_t value = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add a comment about what you are doing here?

bool invalid_bitwise_radix = is_output_bits && (radix != 2);
// Error handling - if num_limbs is zero, value needs to be zero
bool invalid_num_limbs = (num_limbs == 0) && value.is_zero();
bool invalid_num_limbs = (num_limbs == 0) && (!value.is_zero());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

() not needed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping

throw ToRadixException("Invalid parameters for ToRadix");
}

if (is_output_bits) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I preferred using this form because the bits version is likely to be far faster than the non-bits version.

Also, it would be great to not reimplement here and be able to call the other ones?

I understand you reimplemented because you needed to know if it truncated... maybe consider returning std::pair<vector<>, /*truncated*/bool> from the lower level methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making the low level one return a truncated boolean would help cleaning up the constrained one too

return limbs;
}

std::vector<bool> PureToRadix::to_le_bits(const FF& value, uint32_t num_limbs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note that I don't think this one is being exercised in bulk, depending on your confidence you might want to add a test file (until I sort out how to best share the tests)

[or add it to the bulk test via the opcode, if you make the opcode use this]

@@ -44,11 +44,14 @@ void VM_pure_to_radix_memory(State& state)
for (auto _ : state) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Re: line +40]

Can you fix my typo? VM_ -> BM_

See this comment inline on Graphite.

Copy link
Contributor

@jeanmon jeanmon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work overall. Please consider the comments before merging. You might consider the suggested comparison (value and radix to the power num_limbs) to check if a truncation appeared in the simulation code.

pol commit sel_should_decompose;
sel_should_decompose * (1 - sel_should_decompose) = 0;

// On the start row, we define sel_should_decompose as !validation_error && !num_limbs_is_zero
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// On the start row, we define sel_should_decompose as !validation_error && !num_limbs_is_zero
// On the start row, we define sel_should_decompose as !input_validation_error && !num_limbs_is_zero

* (1 - sel_radix_gt_256_err) * (1 - sel_invalid_bitwise_radix)
* (1 - sel_invalid_num_limbs_err);

pol commit output_limb_value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment here about what is output_limb_value and value_found would be welcome.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a rename to limb_value would be less confusing IMHO.

pol commit sel_truncation_error;
sel_truncation_error * (1 - sel_truncation_error) = 0;
// A truncation error happens if in the start row, we look up the to_radix gadget and value_found is off.
// The to radix gadget is little endian, so the first row that we lookup is the last limb. If it's not found in the last limb,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// The to radix gadget is little endian, so the first row that we lookup is the last limb. If it's not found in the last limb,
// The to_radix gadget is little endian, so the first row that we lookup is the last limb. If it's not found in the last limb,


// Negative test: disable decomposition after the start row:
trace.set(Column::to_radix_mem_sel_should_decompose, 2, 0);
EXPECT_THROW_WITH_MESSAGE((check_relation<to_radix_mem>(trace, to_radix_mem::SR_SEL_SHOULD_WRITE_MEM_CONTINUITY)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you want to test another relation or you need to "reset the trace", otherwise it is clear that the relation will fail again.

Copy link
Contributor Author

@sirasistant sirasistant Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah it's a typo. I meant to check the decompose selector continuity. will fix

check_interaction<ToRadixTraceBuilder, lookup_to_radix_mem_check_radix_lt_2_settings>(trace);
}

TEST(ToRadixMemoryConstrainingTest, TruncationError)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we craft a negative test like setting the truncation error while everything is fine and/or the othe way around the error is not toggled when it should?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

{
FF value = FF(0);
for (const auto& limb : limbs) {
value = value * radix + limb.as_ff();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe an explicit cast for radix into FF would not be bad.

memory.set(dst_addr + i, be_output_limbs[i]);
}
// We need to reconstruct the value to check for truncation. The alternative would be to change the interface
// to return a truncated flag.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also raise the radix to the power of num_limbs and compare to the value.
In other words, if value < radix^num_limbs, we know for sure that it is not truncated.

Copy link
Contributor

@jeanmon jeanmon Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to be careful with overflowing the field (or uint256_t) while raising radix to the power of num_limbs though.

},
);

it('Should throw an error for a number that cant be decomposed in the given number of limbs', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('Should throw an error for a number that cant be decomposed in the given number of limbs', async () => {
it('Should throw an error for a number that cannot be decomposed in the given number of limbs', async () => {

Copy link
Contributor

@fcarreiro fcarreiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with the small changes that are left

*/

std::vector<uint8_t> limbs = to_le_radix(value, num_limbs, 2);
const auto& [limbs, truncated] = to_le_radix(value, num_limbs, 2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

danger danger, you want const auto without & here ;)

this only doesn't blow up because of https://stackoverflow.com/questions/39718268/why-do-const-references-extend-the-lifetime-of-rvalues

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah true!

bool invalid_bitwise_radix = is_output_bits && (radix != 2);
// Error handling - if num_limbs is zero, value needs to be zero
bool invalid_num_limbs = (num_limbs == 0) && value.is_zero();
bool invalid_num_limbs = (num_limbs == 0) && (!value.is_zero());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping

@sirasistant sirasistant removed the request for review from LeilaWang September 24, 2025 13:28
if (truncated) {
throw ToRadixException("Truncation error");
}
std::reverse(limbs.begin(), limbs.end());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::ranges::reverse() to be consistent with code below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't work on vectors of bits

@sirasistant sirasistant merged commit f54d94e into merge-train/avm Sep 24, 2025
6 checks passed
@sirasistant sirasistant deleted the arv/to_radix_decomposition branch September 24, 2025 15:11
@AztecBot AztecBot mentioned this pull request Sep 24, 2025
github-merge-queue bot pushed a commit that referenced this pull request Sep 24, 2025
BEGIN_COMMIT_OVERRIDE
feat(AVM): Fail on incomplete toRadix decompositions (#17233)
END_COMMIT_OVERRIDE
mralj pushed a commit that referenced this pull request Oct 13, 2025
ludamad pushed a commit that referenced this pull request Dec 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants